#! /usr/bin/env python
# -*- coding=utf-8 -*-
import BaseHTTPServer, SocketServer, urllib2, zlib, socket
import os, sys, errno, base64, pickle, re, time

try: import ssl;    
except: ssl= None;
try: from OpenSSL import crypto;
except: crypto= None;
# global varibles

def newkey_session(seed):
    return "".join([chr(255-i) for i in xrange(256)])
    
def invkey_session(key):
    return "".join([chr(255-i) for i in xrange(256)])

def trans_enrsa(rsakey, data):
    return data

def trans_dersa(rsakey, data):
    return data

def trans_enfast(data):
    return zlib.compress(data), "1"

def trans_defast(data,key):
    return zlib.decompress(data) 
    
def fetch_proxy(server, method, path, headers, pdata):
    #print "IN >", method, path, "\n","\n".join(["%s:%s"%h for h in headers.iteritems()])
    content, session= trans_enfast(pdata)
    chrome= zlib.compress(pickle.dumps([method,path,headers,session],1))
    package= trans_enrsa(server["key"], chrome) + content    
    header= dict(FETCH_HEADER)    
    header["X-WP-Offset"]= str(len(chrome))
    header["X-WP-Length"]= str(len(pdata))
    
    request= urllib2.Request(server["url"], package, header)
    response= server["opener"].open(request) 
    header= response.headers
    package= response.read()
    response.close()
        
    if header["X-WP-Version"] != "1.0": raise "",""; 
    offset= int(header["X-WP-Offset"])
    chrome= trans_dersa(server["key"], package[:offset]) 
    code, logs, head, session= pickle.loads(zlib.decompress(chrome))
    body= trans_defast(package[offset:], session)
    if int(header["X-WP-Length"]) != len(body): raise "","";
    #print "OUT>", code, logs, "\n","\n".join(["%s:%s"%h for h in head.iteritems()])
    return code, logs, head, body    
        
def fetch_direct(): pass

def nothrow(f):
    def dummy(*pa,**ka):
        try: return dummy(*pa,**ka)
        except RuntimeError, ex:   
            print "EX", ex
    return dummy

class LocalHandler(BaseHTTPServer.BaseHTTPRequestHandler):
    PDATA_LIMIT = 0x100000
    PART_SIZE = 0x100000    
     
    def abort(self, code, logs):
        self.send_error(code, logs);
        self.connection.close()    

    def do_CONNECT(self):
        # setup ssl connection
        if ssl == None: 
            return self.abort(540, "Local: https requires python2.6+")
        host, _, port= self.path.partition(":")
        if port!="" and port!="443": 
            return self.abort(440, "Local: unsupported https port");
        self.wfile.write("HTTP/1.1 200 OK\r\n\r\n")
        keyf, crtf= make_cert(host)            
        
        apt_sock= socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        ssl_sock= ssl.wrap_socket(self.connection, keyf, crtf, True)
                  
        # rewrite request line, url to abs        
        line0, data= '', ssl_sock.read(256);
        while data != '':
            line0+= data
            if '\n' in data:
                line0, data= line0.split('\n',1)
                break
            data= ssl_sock.read(1024)                
        else: return self.abort(402, "Local: malform url. found EOF before CRLF")
        method, path, version= line0.split()        
        if path[0]=="/": path="https://"+self.path+path
        
        apt_sock.connect(PROXY_LISTEN)
        apt_sock.send("%s %s %s\r\n%s"% (method, path, version, data))
        # forward https request and response 
        ssl_sock.settimeout(1)
        try:
            while True:
                data= ssl_sock.read(8192)
                if data=='': break;
                apt_sock.send(data)
        except ssl.SSLError, ex:
            if str(ex).lower().find("timed out") == -1:
                raise #return self.abort(504, str(ex))
        ssl_sock.setblocking(1)
        try:
            while True:
                data= apt_sock.recv(8192)
                if data=='': break;
                ssl_sock.write(data)
        except IOError, ex:        
            if ex.errno!=10053: raise #  return self.abort(504, str(ex))
        # finish 
        ssl_sock.shutdown(socket.SHUT_WR)
        ssl_sock.close()
        apt_sock.close()
        self.connection.close()
   
    def do_METHOD(self):     
        method=self.command; path=self.path;        
            
        # check command path headers postdata               
        if path[0]=='/': path='http://%s%s'%(self.headers['host'],path)         
        if not re.match(r"https?://[^/]+[.:]", path):
            return self.abort(499, "Unsupported Scheme")            
        if method not in ("GET","POST","HEAD","PUT","DELETE"):
            return self.abort(499, "Unsupported Method")
        
        headers= dict(self.headers)        
        if method in ("POST","PUT"):
            lenpdata= int(self.headers.get("content-length", 0))
            if lenpdata > self.PDATA_LIMIT:
                return self.abort(499, "POSTDATA size exceed, Limit by Google")        
            pdata= self.rfile.read(lenpdata) 
            if lenpdata != len(pdata): 
                return self.abort(499, "POSTDATA size does NOT fit Content-Length")
        else: lenpdata=0; pdata="";
        # process headers
        if "cookie" in headers:
            pass
        # send request to GAE fetchserver
        server= SERVER_SEARCH(self.client_address[0], self.path, self.command) 
        try: 
            code, logs, head, body= fetch_proxy(
                server, method, path, headers, pdata)            
        except urllib2.HTTPError, e:
            if e.code == 404:
                self.send_error(404, "Local proxy error, Fetchserver not found at the URL you specified, please check it.")
            elif e.code == 502:
                self.send_error(502, "Local proxy error, Transmission error, or the fetchserver is too busy.")
            else:
                self.send_error(e.code)
            self.connection.close()
            return
        except urllib2.URLError, e:
            print "2", e.errno, e.message
            print method, path, headers
            raise
            self.connection.close()
            return       
        
        # process response        
        head['connection']= "close"
        self.close_connection= 1
        if code not in CUSTOM_CODE:                
            #self.send_response(code&0x0FFF)
            if self.request_version!="HTTP/0.9":
                print code, path
                self.wfile.write("%s %d %s\r\n"%(self.protocol_version, code&0x0FFF, ""))
                for h in head.iteritems():
                    self.wfile.write("%s: %s\r\n"%(h[0].title(), h[1]))
                self.wfile.write("\r\n")
            self.wfile.write(body) 
            self.connection.close() 
        elif code == 299:   
            hrange= re.search(r'bytes\s+(\d+)-(\d+)/(\d+)', head.get("content-range",""))
            if not hrange: 
                return self.abort(499,"Target server do not support range")
            begin, end, total= [int(t) for t in hrange.groups()]
            start=0; stop=total-1; code=206;
            hrange= re.search(r'bytes=(\d*)-(\d*)', headers.get('range',""))
            if hrange: 
                start, stop= [t and int(t) for t in hrange.groups()]                
                # "-500" = the last 500 bytes so start at len-500
                if start==None: start,end= total-end,total-1;
                # "100-" = A many AP so stop at len-1
                if end==None:   start,end= start,total-1;
                if start>stop or stop>=total:
                    return self.abort(499, "Bad range field in request header")                
                head['content-range']='bytes %d-%d/%d'% (start, stop, total);
            else: code=200; del head['content-range'];        
            head['content-length']= str(stop-start+1)
            
            self.send_response(code)
            for h in head.iteritems():
                self.wfile.write('%s: %s\r\n'% h)
            self.wfile.write('\r\n')  #end_headers()        
            if start==begin: # write if the first sertify, or it will be dismiss
                self.wfile.write(body)
                start= end+1            
            print '>>>>>>>>>>>>>>> Range fetch start'            
            failed=0; size= LocalHandler.PART_SIZE;            
            while start<=stop:
                if failed>= LocalHandler.RETRY_RANGE: 
                    return self.abort(499, "too many retry in range fetch")
                headers['range']= 'bytes=%d-%d'% (start, start+size-1)
                code, logs, head, body= fetch_proxy(
                    server, "GET", path, headers, None)
                if code != 206: 
                    failed+=1; time.sleep(2); continue;
                hrange= re.search(r'bytes\s+(\d+)-(\d+)/(\d+)', headers['content-range'])
                if not hrange or int(hrange.group(1))!=start:
                    return self.abort(499, "bad range in response header")
                start= int(hrange.group(2))+1
                print '>>>>>>>>>>>>>>> %s %d'% (headers['content-range'], stop)            
                self.wfile.write(body); failed=0;
            else: print " range fetch succee"
            
        self.connection.close()       
        
    do_GET=   do_METHOD
    do_POST=  do_METHOD
    do_HEAD=  do_METHOD        
    do_PUT=   do_METHOD
    do_DELETE=do_METHOD                  

class DirectHTTPErrorProcessor(urllib2.HTTPErrorProcessor):
    def http_response(self, request, response):
        return response
    https_response = http_response
    
class ThreadingHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
    pass
    
def make_cert(host, CACHE={}, CA=[None, None, 0]):
    if host in CACHE: return CACHE[host];    
    if not crypto or len(host) >= 64:
        return ("certs/__CA__.key", "certs/__CA__.crt")
    fkey, fcrt = "certs/" + host + ".key", "certs/" + host + ".crt"
    CACHE[host] = (fkey, fcrt)
    if not os.path.isfile(fkey) or not os.path.isfile(fcrt):        
        #print(CACHE)
        CPEM = crypto.FILETYPE_PEM
        pkey = crypto.PKey()
        cert = crypto.X509()        
        cert.set_notBefore("20080101120000Z")
        cert.set_notAfter("20180101120000Z")
        subj = cert.get_subject()
        subj.countryName = "CN"
        subj.stateOrProvinceName = "Internet"
        subj.localityName = "Cernet"
        if host != "__CA__":
            #print("create host cert")
            if CA[2] == 0:
                fcakey, fcacrt = make_cert("__CA__")
                CA[0] = crypto.load_privatekey (CPEM, open(fcakey, 'rb').read())
                CA[1] = crypto.load_certificate(CPEM, open(fcacrt, 'rb').read())
                CA[2] = int(open("certs/__CA__.seq", 'rb').read())
            pkey.generate_key(crypto.TYPE_RSA, 1024)
            subj.commonName = host 
            subj.organizationName = host 
            subj.organizationalUnitName = "WallProxy Branch"
            cert.set_serial_number(CA[2])
            cert.set_issuer(CA[1].get_subject()) 
            cert.set_pubkey(pkey)            
            cert.sign(CA[0], "sha1")    
        else: # "init CA cert"
            pkey.generate_key(crypto.TYPE_RSA, 2048)
            subj.commonName = "WallProxy CA" 
            subj.organizationName = "NeverTrust" 
            subj.organizationalUnitName = "WallProxy Root"
            cert.set_serial_number(ord(os.urandom(1)))
            cert.set_issuer(crypto.X509Name(subj))             
            cert.set_pubkey(pkey)
            cert.sign(pkey, "sha1")            
        open(fkey, 'wb').write(crypto.dump_privatekey (CPEM, pkey))
        open(fcrt, 'wb').write(crypto.dump_certificate(CPEM, cert))    
        CA[2]+=1; open("certs/__CA__.seq", "wb").write(str(CA[2])); 
    return fkey, fcrt    

def check_config():
    global GAE_PROXY, PHP_PROXY, LOCAL_PROXY, TIME_STAMP
    global PROXY_LISTEN, FETCH_HEADER, AUTOP_LIST   
    if TIME_STAMP: time.sleep(RELOAD_INTERVAL) 
    timestamp= os.path.getmtime('proxy.conf')
    if TIME_STAMP==timestamp: return True;
    TIME_STAMP= timestamp
    execfile("proxy.conf", globals())    
    
    if isinstance(PROXY_LISTEN, str):
        addr, port= PROXY_LISTEN.split(":")
        PROXY_LISTEN= addr, int(port)
    print "Proxy Running on: %s:%d"% PROXY_LISTEN
    
    if DEFAULT_PROXY:
        OPENER= urllib2.build_opener(HTTP_HANDLER, 
            urllib2.ProxyHandler({"http": DEFAULT_PROXY}))
        server= OPENER.open(LOAD_BALANCE).read().strip()
        print "Connect to Appengine: Adapter %s"% DEFAULT_PROXY
    else:
        try:
            OPENER= urllib2.build_opener(HTTP_HANDLER, urllib2.ProxyHandler({}))           
            server= OPENER.open(LOAD_BALANCE).read().strip()
            print "Connect to Appengine: DIRECT"
        except:
            OPENER= urllib2.build_opener(HTTP_HANDLER, 
                urllib2.ProxyHandler({"http": GOOGLE_PROXY}))
            server= OPENER.open(LOAD_BALANCE).read().strip()  
            print "Connect to Appengine: GOOGLE Adapter"                     
                       
    if not GAE_PROXY: 
        GAE_PROXY=[{"open":OPENER, "url":server, "name":"AUTO", "key":""}]
        print "Using Default Server: %s"% server
    if not PHP_PROXY: PHP_PROXY=[]
    if not LOCAL_PROXY: LOCAL_PROXY=[{}]   
    print "Proxy Channels: GAE[%d] PHP[%d] DIRECT[%d]"% (
        len(GAE_PROXY), len(PHP_PROXY), len(LOCAL_PROXY))
    
    for server in GAE_PROXY: server['type']="GAE";
    for server in PHP_PROXY: server['type']="PHP";
    for server in LOCAL_PROXY: server['type']="LOCAL";    
    for server in GAE_PROXY+PHP_PROXY+LOCAL_PROXY:
        type=server['type']; proxy={};
        if not server.get('url'):
            if type=="LOCAL": server["url"]=",".join(
                [p+":"+v for p,v in server.iteritems() if p!="type"]) or "DIRECT Connection"
        if not server.get('name'):
            server['name']= "%s#%d"%(type,1+globals()[type+"_PROXY"].index(server))             
        if not server.get('key'): 
            server['key']= "";
        if not server.get('opener'): 
            for name, value in server.iteritems():
                if name[0]==":": proxy[name[1:]]= value;
            if type=="LOCAL":
                server['opener']= urllib2.build_opener(
                    DirectHTTPErrorProcessor(), urllib2.ProxyHandler(proxy))
            elif proxy!={}:
                server['opener']= urllib2.build_opener(
                    HTTP_HANDLER, urllib2.ProxyHandler(proxy))
            else: server['opener']= OPENER;
        print "Server %s%s: %s"%(server['name'], server['key']and"(encrypted)"or"", server['url'])       
    
    if isinstance(AUTOP_LIST, str):
        AUTOP_LIST= [i.strip() 
            for i in AUTOP_LIST.split() if i[0]!="#"]
    if AUTOP_LIST: print "auto switch proxy list:\n"+ "\n".join(AUTOP_LIST)
    
    #FIND_PROXY('127.0.0.1', 'http://wallproxy/', 'GET', {})    
    return RELOAD_INTERVAL
    print GAE_PROXY, PHP_PROXY
        
def repeat():
    try: 
        while check_config(): pass;
    except: sys.exit(-1);
    
PROXY_LISTEN= "127.0.0.1:8086"
GAE_PROXY= None
PHP_PROXY= None
LOCAL_PROXY= None   
DEFAULT_PROXY= None
SERVER_SEARCH= lambda*p:GAE_PROXY[0]

AUTOP_LIST= ""
RELOAD_INTERVAL= 10
TIME_STAMP= 0
GOOGLE_PROXY= "www.google.cn:80"
LOAD_BALANCE= "http://gappproxy-center.appspot.com/available_fetchserver.py"
FETCH_HEADER= {"Content-Type":"application/octet-stream", "X-WP-Version":"1.0"}
HTTP_HANDLER= urllib2.HTTPHandler(debuglevel=0)
CUSTOM_CODE= [299]

if __name__ == "__main__":
    print "--------------------------------------------"
    print "Modules: %s, %s"%(
        ssl and"SSL Support"or"no SSL",
        crypto and"OpenSSL Support"or"no OpenSSL")
    check_config() 
    print "--------------------------------------------"
    httpd = ThreadingHTTPServer(PROXY_LISTEN, LocalHandler)
    httpd.serve_forever()
""""""   
        
    

        